﻿namespace CookieTerm
{
    using System;
    using System.Collections.Generic;
    using System.IO;

    public class AnsiInterpreter
    {
        private const int DefaultBufferSize = 4096;
        private const int MaxParameterLength = 256;

        private static Dictionary<char, Type> letterToCommandMapping = new Dictionary<char, Type>
        {
            { 'A', typeof(CursorUp) },
            { 'B', typeof(CursorDown) },
            { 'C', typeof(CursorForward) },
            { 'D', typeof(CursorBack) },
            { 'E', typeof(CursorNextLine) },
            { 'F', typeof(CursorPrevLine) },
            { 'G', typeof(CursorHorizontal) },
            { 'H', typeof(CursorPosition) },
            { 'J', typeof(EraseInDisplay) },
            { 'K', typeof(EraseInLine) },
            { 'L', typeof(InsertLine) },
            { 'm', typeof(SelectGraphicRendition) },
            { 'n', typeof(DeviceStatusReport) },
            { 's', typeof(SaveCursorPosition) },
            { 'u', typeof(RestoreCursorPosition) }

            // Not implemented because mitbbs doesn't use them.
            // { 'S', typeof(ScrollUp) },
            // { 'T', typeof(ScrollDown) },
        };

        // State for tracking ANSI command parsing
        private State state;

        // Variables for tracking text reading
        private byte[] textBuffer;
        private int textLength;

        // Variables for tracking parameter parsing
        private int parameterHolder;
        private int?[] parameters;
        private int parameterLength;
        private bool parameterOmitted;

        public AnsiInterpreter()
        {
            this.state = State.Text;
            this.parameters = new int?[MaxParameterLength];
            this.PrepareToReadNextParam();
        }

        public event EventHandler<AnsiCommandReceivedEventArgs> OnAnsiCommandReceived;

        public event EventHandler<AnsiTextReceivedEventArgs> OnTextReceived;

        /// <summary>
        /// Represents states of ANSI command parsing state machine
        /// </summary>
        private enum State
        {
            /// <summary>
            /// Reading text
            /// </summary>
            Text,

            /// <summary>
            /// ESC read, waiting for [
            /// </summary>
            WaitingForLBR,

            /// <summary>
            /// Parsing ANSI command
            /// </summary>
            Escaped
        }

        public int BufferSize
        {
            set
            {
                this.textBuffer = new byte[value];
            }
        }

        public bool ThrowExceptions { get; set; }

        private byte[] TextBuffer
        {
            get
            {
                if (this.textBuffer == null)
                {
                    this.textBuffer = new byte[DefaultBufferSize];
                }

                return this.textBuffer;
            }
        }

        public void ProcessInput(byte[] value)
        {
            for (int i = 0; i < value.Length; i++)
            {
                this.ProcessByte(value[i]);
            }

            this.FlushOutText();
        }

        private static bool IsAscii(byte b)
        {
            return b < 128;
        }

        private static bool IsLetter(byte b)
        {
            return (b <= 'z' && b >= 'a') || (b <= 'Z' && b >= 'A');
        }

        private static bool IsDigit(byte b)
        {
            return b <= '9' && b >= '0';
        }

        private static int ToDigit(byte b)
        {
            return (int)(b - '0');
        }

        private void ProcessByte(byte value)
        {
            switch (this.state)
            {
                case State.Text:
                    if (value == AsciiByte.ESC)
                    {
                        this.state = State.WaitingForLBR;
                        this.FlushOutText();
                    }
                    else
                    {
                        TextBuffer[this.textLength++] = value;
                        if (this.textLength == DefaultBufferSize)
                        {
                            this.FlushOutText();
                        }
                    }

                    break;
                case State.WaitingForLBR:
                    if (value == AsciiByte.LBR)
                    {
                        this.state = State.Escaped;
                    }
                    else if (ThrowExceptions)
                    {
                        throw new InvalidDataException("Invalid escape sequence, ESC not followed by [.");
                    }

                    break;
                case State.Escaped:
                    if (IsLetter(value))
                    {
                        if (!this.parameterOmitted)
                        {
                            this.parameters[this.parameterLength++] = (int?)this.parameterHolder;
                        }

                        this.FireAnsiCommand(value);

                        this.PrepareToReadNextParam();
                        this.parameterLength = 0;
                        this.state = State.Text;
                    }
                    else if (value == AsciiByte.SEMICOLON)
                    {
                        this.parameters[this.parameterLength++] = this.parameterOmitted ? null : (int?)this.parameterHolder;
                        PrepareToReadNextParam();
                    }
                    else if (IsDigit(value))
                    {
                        this.parameterHolder = this.parameterHolder * 10 + ToDigit(value);
                        this.parameterOmitted = false;
                    }
                    else if (ThrowExceptions)
                    {
                        throw new InvalidDataException(string.Format("Invalid escape sequence: {0}", (char)value));
                    }

                    break;
            }
        }

        private void FlushOutText()
        {
            if (this.textLength == 0)
            {
                return;
            }

            // If the last byte is half of a character, leave it in the buffer for next flush
            bool half = false;

            for (int i = 0; i < this.textLength; i++)
            {
                half = !half && !IsAscii(this.TextBuffer[i]);
            }

            if (this.textLength == 1 && half)
            {
                return;
            }

            byte[] bytes = new byte[half ? this.textLength - 1 : this.textLength];
            Array.Copy(this.TextBuffer, bytes, bytes.Length);

            if (half)
            {
                this.TextBuffer[0] = this.TextBuffer[this.textLength - 1];
                this.textLength = 1;
            }
            else
            {
                this.textLength = 0;
            }

            if (this.OnTextReceived != null)
            {
                this.OnTextReceived(this, new AnsiTextReceivedEventArgs(bytes));
            }
        }

        private void FireAnsiCommand(byte b)
        {
            Type type;
            if (letterToCommandMapping.TryGetValue((char)b, out type))
            {
                AnsiCommand command = (AnsiCommand)Activator.CreateInstance(type);
                command.LoadParameters(this.parameters, this.parameterLength);
                if (this.OnAnsiCommandReceived != null)
                {
                    this.OnAnsiCommandReceived(this, new AnsiCommandReceivedEventArgs(command));
                }
            }
            else if (this.ThrowExceptions)
            {
                throw new NotImplementedException(string.Format("Unrecognized or unsupported ANSI escape {0}", (char)b));
            }
        }

        private void PrepareToReadNextParam()
        {
            this.parameterHolder = 0;
            this.parameterOmitted = true;
        }
    }
}
